package cn.cybozu.api;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.text.ParseException;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;

import javax.activation.FileTypeMap;

import cn.cybozu.common.JsonNode;

/**
 * kintone REST API
 * @author daisuke-ito
 */
public class KintoneRest extends CybozuRest
{
	private static final long NOT_SPACE_APP = 0;
	private static final int DELETE_LIMIT = 100;
	private static final int GET_LIMIT = 500;
	private enum ErrorType
	{
		NORMAL
		,BULK
	}
	/**
	 * GET record
	 * @param appId app id 
	 * @param recordId record id
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 * @throws ParseException
	 */
	public KintoneRecord getRecord(long appId, long recordId)
		throws KintoneRestException, IOException, ParseException
	{
		return getRecord(appId, NOT_SPACE_APP, null, recordId);
	}
	/**
	 * GET record
	 * @param appId app id 
	 * @param apiToken API Token
	 * @param recordId record id
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 * @throws ParseException
	 */
	public KintoneRecord getRecord(long appId, String apiToken, long recordId)
		throws KintoneRestException, IOException, ParseException
	{
		return getRecord(appId, NOT_SPACE_APP, apiToken, recordId);
	}
	/**
	 * GET record
	 * @param appId app id 
	 * @param spaceId space id 
	 * @param recordId record id
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 * @throws ParseException
	 */
	public KintoneRecord getRecord(long appId, long spaceId, long recordId)
			throws KintoneRestException, IOException, ParseException
	{
		return getRecord(appId, spaceId, null, recordId);
	}
	/**
	 * GET record
	 * @param appId app id 
	 * @param spaceId space id 
	 * @param apiToken API Token
	 * @param recordId record id
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 * @throws ParseException
	 */
	public KintoneRecord getRecord(long appId, long spaceId, String apiToken, long recordId)
		throws KintoneRestException, IOException, ParseException
	{
		JsonNode node = restRequest(new URL(getAppUrl(spaceId) + "record.json?app=" + appId + "&id=" + recordId), "GET", null, apiToken);
		return parseRecord(node.get("record"));
	}
	/**
	 * GET RECORDS
	 * @param appId app id
	 * @param query 取得条件
	 * @param fields 取得field code
	 * @param limit 取得最大件数
	 * @param offset offset
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 * @throws ParseException
	 */
	public List<KintoneRecord> getRecords(long appId, String apiToken, String query, String[] fields, int limit, int offset)
			throws KintoneRestException, IOException, ParseException
	{
		return getRecords(appId, NOT_SPACE_APP, apiToken, query, fields, limit, offset);
	}
	/**
	 * GET RECORDS
	 * @param appId app id
	 * @param apiToken API Token
	 * @param query 取得条件
	 * @param fields 取得field code
	 * @param limit 取得最大件数
	 * @param offset offset
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 * @throws ParseException
	 */
	public List<KintoneRecord> getRecords(long appId, long spaceId, String query, String[] fields, int limit, int offset)
			throws KintoneRestException, IOException, ParseException
	{
		return getRecords(appId, spaceId, null, query, fields, limit, offset);
	}
	/**
	 * GET RECORDS
	 * @param appId app id
	 * @param apiToken API Token
	 * @param query 取得条件
	 * @param fields 取得field code
	 * @param limit 取得最大件数
	 * @param offset offset
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 * @throws ParseException
	 */
	public List<KintoneRecord> getRecords(long appId, String query, String[] fields, int limit, int offset)
			throws KintoneRestException, IOException, ParseException
	{
		return getRecords(appId, NOT_SPACE_APP, null, query, fields, limit, offset);
	}
	/**
	 * GET RECORDS
	 * @param appId app id
	 * @param spaceId space id
	 * @param apiToken API Token
	 * @param query 取得条件
	 * @param fields 取得field code
	 * @param limit 取得最大件数
	 * @param offset offset
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 * @throws ParseException
	 */
	public List<KintoneRecord> getRecords(long appId, long spaceId, String apiToken, String query, String[] fields, int limit, int offset)
		throws KintoneRestException, IOException, ParseException
	{
		
		StringBuffer urlString = new StringBuffer(getAppUrl(spaceId) + "records.json?app=" + appId + "&query=" + URLEncoder.encode(query, "UTF8"));
		urlString.append(URLEncoder.encode(" limit " + limit + " offset " + offset, "UTF8"));
		
		for(int i=0; i<fields.length; i++)
		{
			urlString.append("&fields[" + i + "]=" + URLEncoder.encode(fields[i], "UTF8"));
		}
		
		JsonNode node = restRequest(new URL(urlString.toString()), "GET", null, apiToken);
		List<KintoneRecord> records = new ArrayList<KintoneRecord>();
		for(JsonNode recNode: node.get("records").getArray())
		{
			records.add(parseRecord(recNode));
		}
		return records;
	}
	/**
	 * GET RECORDS
	 * @param appId app id
	 * @param query 取得条件
	 * @param fields 取得field code
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 * @throws ParseException
	 */
	public List<KintoneRecord> getAllRecords(long appId, String query, String[] fields)
			throws KintoneRestException, IOException, ParseException
	{
		return getAllRecords(appId, NOT_SPACE_APP, null, query, fields);
	}
	/**
	 * GET RECORDS
	 * @param appId app id
	 * @param spaceId space id
	 * @param query 取得条件
	 * @param fields 取得field code
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 * @throws ParseException
	 */
	public List<KintoneRecord> getAllRecords(long appId, long spaceId, String query, String[] fields)
			throws KintoneRestException, IOException, ParseException
	{
		return getAllRecords(appId, spaceId, null, query, fields);
	}
	/**
	 * GET RECORDS
	 * @param appId app id
	 * @param apiToken API Token
	 * @param query 取得条件
	 * @param fields 取得field code
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 * @throws ParseException
	 */
	public List<KintoneRecord> getAllRecords(long appId, String query, String[] fields, String apiToken)
			throws KintoneRestException, IOException, ParseException
	{
		return getAllRecords(appId, NOT_SPACE_APP, apiToken, query, fields);
	}
	/**
	 * GET RECORDS
	 * @param appId app id
	 * @param spaceId space id
	 * @param apiToken API Token
	 * @param query 取得条件
	 * @param fields 取得field code
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 * @throws ParseException
	 */
	public List<KintoneRecord> getAllRecords(long appId, long spaceId, String apiToken, String query, String[] fields)
			throws KintoneRestException, IOException, ParseException
	{
		List<KintoneRecord> recs = new ArrayList<KintoneRecord>();
		
		Collection<KintoneRecord> tmpRecs = null;
		int offset = 0;
		
		do
		{
			tmpRecs = getRecords(appId, spaceId, apiToken, query, fields, GET_LIMIT, offset);
			recs.addAll(tmpRecs);
			offset += GET_LIMIT;
		}
		while(tmpRecs.size() == GET_LIMIT);
		return recs;
	}
	/**
	 * DELETE record
	 * @param appId app id
	 * @param records records
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void deleteRecord(long appId, KintoneRecord record)
			throws KintoneRestException, IOException
	{
		deleteRecord(appId, NOT_SPACE_APP, null, record);
	}
	/**
	 * DELETE records
	 * @param appId app id
	 * @param apiToken API token
	 * @param records records
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void deleteRecord(long appId, String apiToken, KintoneRecord record)
			throws KintoneRestException, IOException
	{
		deleteRecord(appId, NOT_SPACE_APP, apiToken, record);
	}
	/**
	 * DELETE records
	 * @param appId app id
	 * @param spaceId space id
	 * @param records records
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void deleteRecord(long appId, long spaceId, KintoneRecord record)
			throws KintoneRestException, IOException
	{
		deleteRecord(appId, spaceId, null, record);
	}
	/**
	 * DELETE records 
	 * @param appId app id
	 * @param spaceId space id
	 * @param apiToken API token
	 * @param records records
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void deleteRecord(long appId, long spaceId, String apiToken, KintoneRecord record)
			throws KintoneRestException, IOException
	{
		ArrayList<KintoneRecord> list = new ArrayList<KintoneRecord>();
		list.add(record);	
		deleteRecords(appId, spaceId, apiToken, list);
	}
	/**
	 * DELETE records
	 * @param appId app id
	 * @param records records
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void deleteRecords(long appId, Collection<KintoneRecord> records)
			throws KintoneRestException, IOException
	{
		deleteRecords(appId, NOT_SPACE_APP, null, records);
	}
	/**
	 * DELETE records
	 * @param appId app id
	 * @param apiToken API token
	 * @param records records
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void deleteRecords(long appId, Collection<KintoneRecord> records, String apiToken)
			throws KintoneRestException, IOException
	{
		deleteRecords(appId, NOT_SPACE_APP, apiToken, records);
	}
	/**
	 * DELETE records
	 * @param appId app id
	 * @param spaceId space id
	 * @param records records
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void deleteRecords(long appId, long spaceId, Collection<KintoneRecord> records)
			throws KintoneRestException, IOException
	{
		deleteRecords(appId, spaceId, null, records);
	}
	/**
	 * DELETE records 
	 * @param appId app id
	 * @param spaceId space id
	 * @param apiToken API token
	 * @param records records
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void deleteRecords(long appId, long spaceId, String apiToken, Collection<KintoneRecord> records)
			throws KintoneRestException, IOException
	{
		restRequest(new URL(getAppUrl(spaceId) + "records.json"), "DELETE", getDeletePayloadNode(appId, records), apiToken);
	}
	/**
	 * DELETE records 
	 * @param appId app id
	 * @param spaceId space id
	 * @param apiToken API token
	 * @param records records
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void deleteAllRecords(long appId, long spaceId, String apiToken, Collection<KintoneRecord> records)
			throws KintoneRestException, IOException
	{
		List<KintoneRecord> delRecs = new ArrayList<>();
		
		for(KintoneRecord rec: records)
		{
			delRecs.add(rec);
			if(delRecs.size() == DELETE_LIMIT)
			{
				deleteRecords(appId, spaceId, apiToken, delRecs);
				delRecs.clear();
			}
		}
		if(delRecs.size() != 0)
		{
			deleteRecords(appId, spaceId, apiToken, delRecs);
		}
	}
	/**
	 * POST record
	 * @param appId app id
	 * @param record record
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void postRecord(long appId, KintoneRecord record)
			throws KintoneRestException, IOException
	{
		postRecord(appId, NOT_SPACE_APP, null, record);
	}
	/**
	 * POST record
	 * @param appId app id
	 * @param spaceId space id
	 * @param record record
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void postRecord(long appId, long spaceId, KintoneRecord record)
			throws KintoneRestException, IOException
	{
		postRecord(appId, spaceId, null, record);
	}
	/**
	 * POST record
	 * @param appId app id
	 * @param apiToken API token
	 * @param record record
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void postRecord(long appId, String apiToken, KintoneRecord record)
			throws KintoneRestException, IOException
	{
		
		postRecord(appId, NOT_SPACE_APP, apiToken, record);
	}
	/**
	 * POST record
	 * @param appId app id
	 * @param spaceId space id
	 * @param apiToken API token
	 * @param record record
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void postRecord(long appId, long spaceId, String apiToken, KintoneRecord record)
			throws KintoneRestException, IOException
	{
		setPostRecord(restRequest(new URL(getAppUrl(spaceId) + "record.json"), "POST", getPostPayloadNode(appId, record), apiToken), record);
	}
	/**
	 * POST records
	 * @param appId app id
	 * @param records records
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void postRecords(long appId, Collection<KintoneRecord> records)
			throws KintoneRestException, IOException
	{
		postRecords(appId, NOT_SPACE_APP, null, records);
	}
	/**
	 * POST records
	 * @param appId app id
	 * @param apiToken API token
	 * @param records records
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void postRecords(long appId, String apiToken, Collection<KintoneRecord> records)
			throws KintoneRestException, IOException
	{
		postRecords(appId, NOT_SPACE_APP, apiToken, records);
	}
	/**
	 * POST records
	 * @param appId app id
	 * @param spaceId space id
	 * @param records records
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void postRecords(long appId, long spaceId, Collection<KintoneRecord> records)
			throws KintoneRestException, IOException
	{
		postRecords(appId, spaceId, null, records);
	}
	/**
	 * POST records
	 * @param appId app id
	 * @param spaceId space id
	 * @param apiToken API token
	 * @param records records
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void postRecords(long appId, long spaceId, String apiToken, Collection<KintoneRecord> records)
			throws KintoneRestException, IOException
	{
		setPostRecord(restRequest(new URL(getAppUrl(spaceId) + "records.json"), "POST", getPostPayloadNode(appId, records), apiToken), records);
	}
	/**
	 * PUT record
	 * @param appId app id
	 * @param records record
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void putRecord(long appId, KintoneRecord record)
			throws KintoneRestException, IOException
	{
		putRecord(appId, NOT_SPACE_APP, null, record);
	}
	/**
	 * PUT record
	 * @param appId app id
	 * @param spaceId space id
	 * @param records record
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void putRecord(long appId, long spaceId, KintoneRecord record)
			throws KintoneRestException, IOException
	{
		putRecord(appId, spaceId, null, record);
	}
	/**
	 * PUT record
	 * @param appId app id
	 * @param apiToken API token
	 * @param records record
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void putRecord(long appId, String apiToken, KintoneRecord record)
			throws KintoneRestException, IOException
	{
		putRecord(appId, NOT_SPACE_APP, apiToken, record);
	}
	/**
	 * PUT record
	 * @param appId app id
	 * @param spaceId space id
	 * @param apiToken API token
	 * @param records record
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void putRecord(long appId, long spaceId, String apiToken, KintoneRecord record)
			throws KintoneRestException, IOException
	{
		setPutRecord(restRequest(new URL(getAppUrl(spaceId) + "record.json"), "PUT", getPutPayloadNode(appId, record), apiToken), record);
	}
	/**
	 * PUT records
	 * @param appId app id
	 * @param records records
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void putRecords(long appId, Collection<KintoneRecord> records)
			throws KintoneRestException, IOException
	{
		putRecords(appId, NOT_SPACE_APP, null, records);
	}
	/**
	 * PUT records
	 * @param appId app id
	 * @param spaceId space id
	 * @param records records
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void putRecords(long appId, long spaceId, Collection<KintoneRecord> records)
			throws KintoneRestException, IOException
	{
		putRecords(appId, spaceId, null, records);
	}
	/**
	 * PUT records
	 * @param appId app id
	 * @param apiToken API token
	 * @param records records
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void putRecords(long appId, String apiToken, Collection<KintoneRecord> records)
			throws KintoneRestException, IOException
	{
		putRecords(appId, NOT_SPACE_APP, apiToken, records);
	}
	/**
	 * PUT records
	 * @param appId app id
	 * @param spaceId space id
	 * @param apiToken API token
	 * @param records records
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	public void putRecords(long appId, long spaceId, String apiToken, Collection<KintoneRecord> records)
			throws KintoneRestException, IOException
	{
		setPutRecord(restRequest(new URL(getAppUrl(spaceId) + "records.json"), "PUT", getPutPayloadNode(appId, records), apiToken), records);
	}
	/**
	 * Download file
	 * @param file file instance
	 * @param dir saved directory
	 * @throws IOException
	 * @throws KintoneRestException
	 * @throws ParseException
	 */
	public void downloadFile(long spaceId, String apiToken, CybozuFile file, File dir)
		throws IOException, KintoneRestException, ParseException
	{
		HttpURLConnection urlconn = null;
		DataInputStream is = null;
		DataOutputStream os = null;
		
		BufferedReader reader = null;
		
		try
		{
			String urlString = getUrl() + "/k/" + (spaceId != NOT_SPACE_APP ? "guest/" + spaceId + "/" : "") + "v1/file.json?fileKey=" + file.getFileKey();
			
			URL url = new URL(urlString);
			if(isDebugMode())
			{
				System.out.println(urlString);
			}
			
			urlconn = (HttpURLConnection)url.openConnection();
			urlconn.setDoInput(true);
			urlconn.setUseCaches(false);
			
			urlconn.setRequestMethod("GET");
			setRequestHeader(urlconn, apiToken);
			//setBasicAuth(client);
			
			urlconn.connect();
			
			if(urlconn.getResponseCode() == HttpURLConnection.HTTP_OK)
			{
				// Input Stream
				is = new DataInputStream(urlconn.getInputStream());
				
				// Output Stream
				os = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(new File(dir, file.getName()))));
				
				// Read Data
				byte[] b = new byte[4096];
				int readByte = 0;
				
				while(-1 != (readByte = is.read(b)))
				{
					os.write(b, 0, readByte);
				}
			}
			else
			{
				reader = new BufferedReader(new InputStreamReader(urlconn.getErrorStream(), Charset.forName("UTF-8")));
				
				StringBuilder sb = new StringBuilder();
				while(true)
				{
					String line = reader.readLine();
					if( line == null )
					{
						break;
					}
					sb.append(line);
				}
				throw createException(JsonNode.parse(sb.toString()));
			}
		}
		finally
		{
			if(reader != null)
			{
				reader.close();
			}
			if(is != null)
			{
				is.close();
			}
			if(os != null)
			{
				os.close();
			}
			if(urlconn != null)
			{
				urlconn.disconnect();
			}
		}
	}
	/**
	 * Download file
	 * @param file file instance
	 * @param dir saved directory
	 * @throws IOException
	 * @throws KintoneRestException
	 * @throws ParseException
	 */
	public void downloadFile(String apiToken, CybozuFile file, File dir)
		throws IOException, KintoneRestException, ParseException
	{
		downloadFile(NOT_SPACE_APP, apiToken, file, dir);
	}
	/**
	 * Download file
	 * @param file file instance
	 * @param dir saved directory
	 * @throws IOException
	 * @throws KintoneRestException
	 * @throws ParseException
	 */
	public void downloadFile(long spaceId, CybozuFile file, File dir)
		throws IOException, KintoneRestException, ParseException
	{
		downloadFile(spaceId, null, file, dir);
	}
	/**
	 * Download file
	 * @param file file instance
	 * @param dir saved directory
	 * @throws IOException
	 * @throws KintoneRestException
	 * @throws ParseException
	 */
	public void downloadFile(CybozuFile file, File dir)
		throws IOException, KintoneRestException, ParseException
	{
		downloadFile(NOT_SPACE_APP, null, file, dir);
	}
	/**
	 * Post file
	 * @param apiToken apiToken
	 * @param file file path
	 * @return file key
	 * @throws IOException
	 * @throws KintoneRestException
	 */
	public String uploadFile(String apiToken, File file)
			throws IOException, KintoneRestException
	{
		return uploadFile(NOT_SPACE_APP, apiToken, file);
	}
	/**
	 * Post file
	 * @param apiToken apiToken
	 * @param file file path
	 * @return file key
	 * @throws IOException
	 * @throws KintoneRestException
	 */
	public String uploadFile(long spaceId, File file)
			throws IOException, KintoneRestException
	{
		return uploadFile(NOT_SPACE_APP, null, file);
	}
	/**
	 * Post file
	 * @param spaceId spaceId
	 * @param apiToken apiToken
	 * @param file file path
	 * @return file key
	 * @throws IOException
	 * @throws KintoneRestException
	 */
	public String uploadFile(long spaceId, String apiToken, File file)
				throws IOException, KintoneRestException
	{
		final String TWO_HYPHEN = "--";
		final String EOL = "\r\n";
		final String BOURDARY = String.format("%x", new Random().hashCode());
		final String CHARSET = "UTF-8";
		
		HttpURLConnection urlconn = null;
		DataOutputStream os = null;
		FileInputStream  is = null;
		BufferedReader reader = null;
		
		// 送信するコンテンツを成形する
		StringBuilder contentsBuilder = new StringBuilder();
		String closingContents = String.format("%s%s%s%s%s", EOL, TWO_HYPHEN, BOURDARY, TWO_HYPHEN, EOL);
		
		int iContentsLength = 0;
		String filePath = file.getName();
		
		// ファイル情報のセット
		contentsBuilder.append(String.format("%s%s%s", TWO_HYPHEN, BOURDARY, EOL));
		contentsBuilder.append(String.format("Content-Disposition: form-data; name=\"file\"; filename=\"%s\"%s", filePath, EOL));
		//contentsBuilder.append(String.format("Content-Type: %s%s", "application/upload", EOL));
		contentsBuilder.append(String.format("Content-Type: %s%s", FileTypeMap.getDefaultFileTypeMap().getContentType(file), EOL));
		contentsBuilder.append(EOL);
		
		// ファイルサイズ
		iContentsLength += contentsBuilder.toString().getBytes(CHARSET).length;
		iContentsLength += file.length();
		iContentsLength += closingContents.getBytes(CHARSET).length;
		
		try
		{
			String urlString = getUrl() + "/k" + (spaceId == NOT_SPACE_APP ? "" : "/guest/" + spaceId) + "/v1/file.json";
			
			URL url = new URL(urlString);
			
			urlconn = (HttpURLConnection)url.openConnection();
			urlconn.setDoInput(true);
			urlconn.setDoOutput(true);
			
			// キャッシュを使用しない
			urlconn.setUseCaches(false);
			// HTTPストリーミングを有効にする
			urlconn.setChunkedStreamingMode(0);
			
			urlconn.setRequestMethod("POST");
			
			// 持続接続を設定
			urlconn.setRequestProperty("Connection", "Keep-Alive");
			
	        // POSTデータの形式を設定
			urlconn.setRequestProperty("Content-Type", String.format("multipart/form-data; boundary=%s", BOURDARY));
			// POSTデータの長さを設定
			urlconn.setRequestProperty("Content-Length", String.valueOf(iContentsLength));
			
			setRequestHeader(urlconn, apiToken);
			//setBasicAuth(client);
			
			os = new DataOutputStream(urlconn.getOutputStream());
			
			os.write(contentsBuilder.toString().getBytes(CHARSET));
			
			byte buffer[] = new byte[1024];
			is = new FileInputStream(file);
			
			int writeByte = 0;
			
			while((writeByte = is.read(buffer, 0, buffer.length)) > -1)
			{
				os.write(buffer, 0, writeByte);
			}
			os.write(closingContents.getBytes(CHARSET));
			
			boolean isSuccess = urlconn.getResponseCode() == HttpURLConnection.HTTP_OK;
			
			reader = new BufferedReader(new InputStreamReader(isSuccess ? urlconn.getInputStream() : urlconn.getErrorStream(), Charset.forName("UTF-8")));
			
			StringBuilder sb = new StringBuilder();
			while(true)
			{
				String line = reader.readLine();
				if( line == null )
				{
					break;
				}
				sb.append(line);
			}
			if(isDebugMode())
			{
				System.out.println(sb.toString());
			}
			if(isSuccess)
			{
				JsonNode n = new JsonNode(sb.toString());
				 
				n = n.get("fileKey");
				return JsonNode.parse(sb.toString()).get("fileKey").getTextValue();
			}
			else
			{
				throw createException(JsonNode.parse(sb.toString()));
			}
		}
		finally
		{
			if(reader != null)
			{
				reader.close();
			}
			if(is != null)
			{
				is.close();
			}
			if(os != null)
			{
				os.close();
			}
			if(urlconn != null)
			{
				urlconn.disconnect();
			}
		}
	}
	/**
	 * 一括request
	 * @param requests
	 * @throws KintoneRestException
	 * @throws IOException
	 * @throws ParseException
	 */
	public void bulkRequest(List<KintoneRequest> requests)
		throws KintoneRestException, IOException, ParseException
	{
		bulkRequest(requests, NOT_SPACE_APP);
	}
	/**
	 * 一括request
	 * @param requests
	 * @throws KintoneRestException
	 * @throws IOException
	 * @throws ParseException
	 */
	public void bulkRequest(List<KintoneRequest> requests, long spaceId)
		throws KintoneRestException, IOException, ParseException
	{
		JsonNode node = new JsonNode();
		
		List<JsonNode> requestNodes = new ArrayList<JsonNode>();
		for(KintoneRequest request: requests)
		{
			JsonNode requestNode = new JsonNode();
			String method = request.getMethod().toString();
			
			if(method.equals("GET"))
			{
				throw new KintoneRestException(method + " is unsuppored.");
			}
			
			requestNode.setJsonNode("method", new JsonNode(method));
			
			JsonNode payloadNode = null;
			long appId = request.getAppId();
			
			if(request.getRecord() != null)
			{
				requestNode.setJsonNode("api", new JsonNode("/k/v1/record.json"));
				
				switch(method)
				{
				case "POST":
					payloadNode = getPostPayloadNode(appId, request.getRecord());
					break;
				case "PUT":
					payloadNode = getPutPayloadNode(appId, request.getRecord());
					break;
				case "DELETE":
					throw new KintoneRestException(method + " is unsuppored.");
				}
			}
			else
			{
				requestNode.setJsonNode("api", new JsonNode("/k/v1/records.json"));
				
				switch(method)
				{
				case "POST":
					payloadNode = getPostPayloadNode(appId, request.getRecords());
					break;
				case "PUT":
					payloadNode = getPutPayloadNode(appId, request.getRecords());
					break;
				case "DELETE":
					payloadNode = getDeletePayloadNode(appId, request.getRecords());
					break;
				}
			}
			
			requestNode.setJsonNode("payload", payloadNode);
			requestNodes.add(requestNode);
		}
		
		node.setJsonNode("requests", new JsonNode(requestNodes));
		
		JsonNode resNode = restRequest(new URL(getAppUrl(spaceId) + "bulkRequest.json"), "POST", node, null);
		
		List<JsonNode> resps = resNode.get("results").getArray();
		
		for(int i=0; i<requests.size(); i++)
		{
			KintoneRequest request = requests.get(i);
			String method = request.getMethod().toString();
			
			if(request.getRecord() != null)
			{
				switch(method)
				{
				case "POST":
					setPostRecord(resps.get(i), request.getRecord());
					break;
				case "PUT":
					setPutRecord(resps.get(i), request.getRecord());
					break;
				}
			}
			else
			{
				switch(method)
				{
				case "POST":
					setPostRecord(resps.get(i), request.getRecords());
					break;
				case "PUT":
					setPutRecord(resps.get(i), request.getRecords());
					break;
				}
			}
		}
	}
	/**
	 * 
	 * @param appId
	 * @param entities
	 * @throws KintoneRestException
	 * @throws IOException
	 * @throws ParseException
	 */
	public void putAppAcl(long appId, List<KintoneAppAclEntity> entities)
			throws KintoneRestException, IOException, ParseException
	{
		putAppAcl(appId, NOT_SPACE_APP, null, entities);
	}
	/**
	 * 
	 * @param appId
	 * @param entities
	 * @param spaceId
	 * @param apiToken
	 * @throws KintoneRestException
	 * @throws IOException
	 * @throws ParseException
	 */
	public void putAppAcl(long appId, long spaceId, String apiToken, List<KintoneAppAclEntity> entities)
			throws KintoneRestException, IOException, ParseException
	{
		JsonNode node = new JsonNode();
		node.setJsonNode("app", new JsonNode(appId));
		
		List<JsonNode> rights = new ArrayList<JsonNode>();
		
		for(KintoneAppAclEntity entity: entities)
		{
			JsonNode rightNode = new JsonNode();
			
			JsonNode entityNode = getEntityNode(entity.getEntity());
			
			rightNode.setJsonNode("entity", entityNode);
			if(entityNode.get("type").getTextValue().equals("ORGANIZATION"))
			{
				rightNode.setJsonNode("includeSubs", new JsonNode(entity.isIncludeSubs()));
			}
			rightNode.setJsonNode("appEditable", new JsonNode(entity.isAppEditable()));
			rightNode.setJsonNode("recordViewable", new JsonNode(entity.isRecordViewable()));
			rightNode.setJsonNode("recordAddable", new JsonNode(entity.isRecordAddable()));
			rightNode.setJsonNode("recordEditable", new JsonNode(entity.isRecordEditable()));
			rightNode.setJsonNode("recordDeletable", new JsonNode(entity.isRecordDeletable()));
			rightNode.setJsonNode("recordImportable", new JsonNode(entity.isRecordImportable()));
			rightNode.setJsonNode("recordExportable", new JsonNode(entity.isRecordExportable()));
			rights.add(rightNode);
		}
		node.setJsonNode("rights", new JsonNode(rights));
		restRequest(new URL(getAppUrl(spaceId) + "/app/acl.json"), "PUT", node, apiToken);
	}
	/**
	 * 
	 * @param appId
	 * @param acls
	 * @throws KintoneRestException
	 * @throws IOException
	 * @throws ParseException
	 */
	public void putRecordAcl(long appId, List<KintoneRecordAcl> acls)
			throws KintoneRestException, IOException, ParseException
	{
		putRecordAcl(appId, NOT_SPACE_APP, null, acls);
	}
	/**
	 * 
	 * @param appId
	 * @param acls
	 * @param spaceId
	 * @param apiToken
	 * @throws KintoneRestException
	 * @throws IOException
	 * @throws ParseException
	 */
	public void putRecordAcl(long appId, long spaceId, String apiToken, List<KintoneRecordAcl> acls)
			throws KintoneRestException, IOException, ParseException
	{
		JsonNode node = new JsonNode();
		node.setJsonNode("app", new JsonNode(appId));	// 5.7以降から動作
			
		List<JsonNode> rights = new ArrayList<JsonNode>();
		
		for(KintoneRecordAcl acl: acls)
		{
			JsonNode rightNode = new JsonNode();
			
			if(acl.getCondition() != null)
			{
				rightNode.setJsonNode("filterCond", new JsonNode(acl.getCondition()));	
			}
			List<JsonNode> entities = new ArrayList<JsonNode>();
			
			for(KintoneRecordAclEntity entity: acl)
			{
				JsonNode entityNode = new JsonNode();
				JsonNode objectNode = getEntityNode(entity.getEntity());
				
				entityNode.setJsonNode("entity", objectNode);
				if(objectNode.get("type").getTextValue().equals("ORGANIZATION"))
				{
					entityNode.setJsonNode("includeSubs", new JsonNode(entity.isIncludeSubs()));
				}
				entityNode.setJsonNode("viewable", new JsonNode(entity.isViewable()));
				entityNode.setJsonNode("editable", new JsonNode(entity.isEditable()));
				entityNode.setJsonNode("deletable", new JsonNode(entity.isDeletable()));
				entities.add(entityNode);
			}
			rightNode.setJsonNode("entities", new JsonNode(entities));
			rights.add(rightNode);
		}
		node.setJsonNode("rights", new JsonNode(rights));
		restRequest(new URL(getAppUrl(spaceId) + "/record/acl.json"), "PUT", node, apiToken);
	}
	public void putFieldAcl(long appId, List<KintoneFieldAcl> acls)
			throws KintoneRestException, IOException, ParseException
	{
		putFieldAcl(appId, NOT_SPACE_APP, null, acls);
	}
	
	public void putFieldAcl(long appId, long spaceId, String apiToken, List<KintoneFieldAcl> acls)
			throws KintoneRestException, IOException, ParseException
	{
		JsonNode node = new JsonNode();
		node.setJsonNode("app", new JsonNode(appId));	// 5.7以降から動作
			
		List<JsonNode> rights = new ArrayList<JsonNode>();
		
		for(KintoneFieldAcl acl: acls)
		{
			JsonNode rightNode = new JsonNode();
			
			rightNode.setJsonNode("code", new JsonNode(acl.getFieldCode()));
			
			List<JsonNode> entities = new ArrayList<JsonNode>();
			
			for(KintoneFieldAclEntity entity: acl)
			{
				JsonNode entityNode = new JsonNode();
				JsonNode objectNode = getEntityNode(entity.getEntity());
				
				entityNode.setJsonNode("entity", objectNode);
				if(objectNode.get("type").getTextValue().equals("ORGANIZATION"))
				{
					entityNode.setJsonNode("includeSubs", new JsonNode(entity.isIncludeSubs()));
				}
				if(entity.isEditable())
				{
					entityNode.setJsonNode("accessibility", new JsonNode("WRITE"));
				}
				else if(entity.isViewable())
				{
					entityNode.setJsonNode("accessibility", new JsonNode("READ"));
				}
				else
				{
					entityNode.setJsonNode("accessibility", new JsonNode("NODE"));
				}
				entities.add(entityNode);
			}
			rightNode.setJsonNode("entities", new JsonNode(entities));
			rights.add(rightNode);
		}
		node.setJsonNode("rights", new JsonNode(rights));
		restRequest(new URL(getAppUrl(spaceId) + "/field/acl.json"), "PUT", node, apiToken);
	}






	
	
	
	private JsonNode getPostPayloadNode(long appId, KintoneRecord record)
	{
		JsonNode node = new JsonNode();
		node.setJsonNode("app", new JsonNode(appId));
		node.setJsonNode("record", formatJsonNode(record));
		return node;
	}
	private JsonNode getPostPayloadNode(long appId, Collection<KintoneRecord> records)
	{
		JsonNode node = new JsonNode();
		node.setJsonNode("app", new JsonNode(appId));
		
		List<JsonNode> recsList = new ArrayList<JsonNode>();
		
		for(KintoneRecord rec : records)
		{
			JsonNode recNode = formatJsonNode(rec);
			
			if(recNode.get("$id") != null)
			{
				recNode.removeJsonNode("$id");
			}
			if(recNode.get("$revision") != null)
			{
				recNode.removeJsonNode("$revision");
			}
			recsList.add(formatJsonNode(rec));
		}
		
		node.setJsonNode("records", new JsonNode(recsList));
		return node;
	}
	private JsonNode getPutPayloadNode(long appId, KintoneRecord record)
	{
		JsonNode node = getPostPayloadNode(appId, record);
		node.setJsonNode("id", new JsonNode(record.getId()));
		
		if(record.getRevision() != null)
		{
			node.setJsonNode("revision", new JsonNode(record.getRevision()));
		}
		return node;
	}
	private JsonNode getPutPayloadNode(long appId, Collection<KintoneRecord> records)
	{
		JsonNode node = new JsonNode();
		node.setJsonNode("app", new JsonNode(appId));
		
		List<JsonNode> nodeList = new ArrayList<JsonNode>();
	
		for(KintoneRecord record: records)
		{
			JsonNode recNode = new JsonNode();
			recNode.setJsonNode("record", formatJsonNode(record));		
			recNode.setJsonNode("id", new JsonNode(record.getId()));
			
			if(record.getRevision() != null)
			{
				recNode.setJsonNode("revision", new JsonNode(record.getRevision()));
			}
			nodeList.add(recNode);
		}
		node.setJsonNode("records", new JsonNode(nodeList));
		return node;
	}
	
	private JsonNode getDeletePayloadNode(long appId, Collection<KintoneRecord> records)
	{
		JsonNode node = new JsonNode();
		node.setJsonNode("app", new JsonNode(appId));
		
		List<JsonNode> idsList = new ArrayList<JsonNode>();
		List<JsonNode> revList = new ArrayList<JsonNode>();
		
		for(KintoneRecord rec : records)
		{
			idsList.add(new JsonNode(rec.getId()));
			
			if(rec.getRevision() != null)
			{
				revList.add(new JsonNode(rec.getRevision()));
			}
		}
		node.setJsonNode("ids", new JsonNode(idsList));
		if(revList.size() != 0)
		{
			node.setJsonNode("revisions", new JsonNode(revList));
		}
		return node;
	}
    		
	private void setPostRecord(JsonNode node, KintoneRecord record)
	{
		record.setId(node.get("id").getLongValue());
		record.setRevision(node.get("revision").getLongValue());
	}
	private void setPutRecord(JsonNode node, KintoneRecord record)
	{
		record.setRevision(node.get("revision").getLongValue());
	}
	private void setPostRecord(JsonNode node, Collection<KintoneRecord> records)
	{
		List<JsonNode> ids = node.get("ids").getArray();
		List<JsonNode> revisions = node.get("revisions").getArray();
		
		int i = 0;
		for(KintoneRecord rec : records)
		{
			rec.setId(ids.get(i).getLongValue());
			rec.setRevision(revisions.get(i).getLongValue());
			i++;
		}
	}
	private void setPutRecord(JsonNode node, Collection<KintoneRecord> records)
	{
		List<JsonNode> reqs = node.get("records").getArray();
		int i = 0;
		for(KintoneRecord rec : records)
		{
			rec.setId(reqs.get(i).get("id").getLongValue());
			rec.setRevision(reqs.get(i).get("revision").getLongValue());
			i++;
		}
	}
	/**
	 * recordの解析
	 * @param rootNode root node
	 * @return record
	 * @throws ParseException
	 */
	private KintoneRecord parseRecord(JsonNode rootNode)
		throws ParseException
	{
		KintoneRecord record = new KintoneRecord();
		
		for(Iterator<Entry<String, JsonNode>> it = rootNode.getMap().entrySet().iterator(); it.hasNext();)
		{
			Entry<String, JsonNode> entry = it.next();
			
			String name = entry.getKey();
			
			if(name.equals("Count") || name.equals(","))
			{
				continue;
			}
			JsonNode valueObj = entry.getValue();
			JsonNode valueNode = valueObj.get("value");
			String type = valueObj.get("type").getTextValue();
			
			// TIME
			if(type.equals("SINGLE_LINE_TEXT") || type.equals("RADIO_BUTTON") 
					|| type.equals("DROP_DOWN") || type.equals("RICH_TEXT")
					|| type.equals("RECORD_NUMBER") || type.equals("MULTI_LINE_TEXT")
					|| type.equals("LINK") || type.equals("STATUS")
			)
			{	
				if(!valueNode.isNull() && valueNode.getTextValue().length() != 0)
				{
					record.setString(name, valueNode.getTextValue());
				}
				else
				{
					record.setNull(name);
				}
			}
			else if(type.equals("NUMBER"))
			{
				if(!valueNode.isNull() && valueNode.getTextValue().length() != 0)
				{
					record.setNumber(name, new BigDecimal(valueNode.getTextValue()));
				}
				else
				{
					record.setNull(name);
				}
			}
			else if(type.equals("DATE"))
			{
				if(!valueNode.isNull() && valueNode.getTextValue().length() != 0)
				{
					record.setDate(name, LocalDate.parse(valueNode.getTextValue(), DateTimeFormatter.ISO_LOCAL_DATE));
				}
				else
				{
					record.setNull(name);
				}
			}
			else if(type.equals("UPDATED_TIME") || type.equals("CREATED_TIME") || type.equals("DATETIME"))
			{
				if(!valueNode.isNull() && valueNode.getTextValue().length() != 0)
				{
					record.setOffsetDateTime(name, OffsetDateTime.parse(valueNode.getTextValue()));
				}
				else
				{
					record.setNull(name);
				}
			}
			else if(type.equals("CALC"))
			{
				//日時: "2012-01-11T11:30:00Z"
				//日付: "2012-01-11"
				//時刻: "11:30"
				//時間（時、分）: "31時間30分"
				//時間（日、時、分）: "1日20時間30分"	
				if(!valueNode.isNull() && valueNode.getTextValue().length() != 0)
				{
					try
					{
						record.setNumber(name, new BigDecimal(valueNode.getTextValue()));
					}
					catch(NumberFormatException e)
					{
						record.setString(name, valueNode.getTextValue());
					}
				}
				else
				{
					record.setNull(name);
				}
			}
			else if(type.equals("USER_SELECT") || type.equals("STATUS_ASSIGNEE"))
			{
				if(!valueNode.isNull())
				{
					record.setUsers(name, parseUserList(valueNode));
				}
				else
				{
					record.setUsers(name, new KintoneUserMap());
				}
			}
			else if(type.equals("GROUP_SELECT"))
			{
				if(!valueNode.isNull())
				{
					record.setGroups(name, parseGroupList(valueNode));
				}
				else
				{
					record.setGroups(name, new KintoneGroupMap());
				}
			}
			else if(type.equals("ORGANIZATION_SELECT"))
			{
				if(!valueNode.isNull())
				{
					record.setOrganizations(name, parseOrganizationList(valueNode));
				}
				else
				{
					record.setOrganizations(name, new KintoneOrganizationMap());
				}
			}
			else if(type.equals("SUBTABLE")){
				
				KintoneSubTable subTable = new KintoneSubTable();
				if(!valueNode.isNull())
				{
					for(JsonNode node : valueNode.getArray())
					{
						KintoneRecord subRec = parseRecord(node.get("value"));
						subRec.setId(node.get("id").getLongValue());
						subTable.add(subRec);
					}
				}
				record.setSubTable(name, subTable);
			}
			else if(type.equals("FILE"))
			{
				if(!valueNode.isNull())
				{
					record.setFiles(name, parseFileList(valueNode));
				}
				else
				{
					record.setNull(name);
				}
			}
			else if(type.equals("CHECK_BOX") || type.equals("MULTI_SELECT") || type.equals("CATEGORY"))
			{
				if(!valueNode.isNull())
				{
					record.setStringSet(name, parseStringSet(valueNode));
				}
				else
				{
					record.setStringSet(name, new HashSet<String>());
				}
			}
			else if(type.equals("TIME"))
			{
				if(!valueNode.isNull() && valueNode.getTextValue().length() != 0)
				{
					record.setTime(name, LocalTime.parse(valueNode.getTextValue()));
				}
				else
				{
					record.setNull(name);
				}
			}
			else if(type.equals("__ID__"))
			{
				record.setId(Long.parseLong(valueNode.getTextValue()));
			}
			else if(type.equals("__REVISION__"))
			{
				record.setRevision(Long.parseLong(valueNode.getTextValue()));
			}
			else if(type.equals("CREATOR") || type.equals("MODIFIER"))
			{
				CybozuUser user = new CybozuUser();
				user.setCode(valueNode.get("code").getTextValue());
				user.setName(valueNode.get("name").getTextValue());
				record.setUser(name, user);
			}
		}
		return record;
	}
	@SuppressWarnings("unchecked")
	protected JsonNode formatJsonNode(KintoneRecord record)
	{
		JsonNode recNode = new JsonNode();
		
		for(Entry<String, Object> entry: record.getFields().entrySet())
		{
			String code = entry.getKey();
			Object value = entry.getValue();
			JsonNode codeNode  = new JsonNode();
			JsonNode valueNode = new JsonNode();
			
			if(value == null)
			{
			}
			else if(value instanceof String)
			{
				valueNode.setValue(value);
			}
			else if (value instanceof LocalDate)
			{
				valueNode.setValue(((LocalDate)value).format(DATE_FORMATTER));
			}
			else if (value instanceof LocalTime)
			{
				valueNode.setValue(((LocalTime)value).format(TIME_FORMATTER));
			}
			else if (value instanceof ZonedDateTime)
			{				
				valueNode.setValue(((ZonedDateTime)value).format(DATETIME_FORMATTER));
			}
			else if (value instanceof OffsetDateTime)
			{				
				valueNode.setValue(((OffsetDateTime)value).format(DATETIME_FORMATTER));
			}
			else if(value instanceof KintoneUserMap)
			{
				List<JsonNode> list = new ArrayList<JsonNode>();
				for(String user : ((KintoneUserMap)value).keySet())
				{
					JsonNode userNode = new JsonNode();
					JsonNode userCodeNode = new JsonNode();
					userCodeNode.setValue(user);
					userNode.setJsonNode("code", userCodeNode);
					list.add(userNode);
				}
				valueNode.setArray(list);
			}
			else if(value instanceof KintoneGroupMap)
			{
				List<JsonNode> list = new ArrayList<JsonNode>();
				for(String user : ((KintoneGroupMap)value).keySet())
				{
					JsonNode userNode = new JsonNode();
					JsonNode userCodeNode = new JsonNode();
					userCodeNode.setValue(user);
					userNode.setJsonNode("code", userCodeNode);
					list.add(userNode);
				}
				valueNode.setArray(list);
			}
			else if(value instanceof KintoneOrganizationMap)
			{
				List<JsonNode> list = new ArrayList<JsonNode>();
				for(String user : ((KintoneOrganizationMap)value).keySet())
				{
					JsonNode userNode = new JsonNode();
					JsonNode userCodeNode = new JsonNode();
					userCodeNode.setValue(user);
					userNode.setJsonNode("code", userCodeNode);
					list.add(userNode);
				}
				valueNode.setArray(list);
			}
			else if(value instanceof Set)
			{
				List<JsonNode> list = new ArrayList<JsonNode>();
				
				for(String str : ((Set<String>)value))
				{
					JsonNode strNode = new JsonNode();
					strNode.setValue(str);
					list.add(strNode);
				}
				valueNode.setArray(list);
			}
			else if(value instanceof KintoneFileList)
			{
				List<JsonNode> list = new ArrayList<JsonNode>();
				
				for(CybozuFile file : ((KintoneFileList)value))
				{
					JsonNode fileNode = new JsonNode();
					JsonNode fileKeyNode = new JsonNode();
					fileKeyNode.setValue(file.getFileKey());
					fileNode.setJsonNode("fileKey", fileKeyNode);
					
					list.add(fileNode);
				}
				valueNode.setArray(list);
			}
			else if(value instanceof CybozuUser)
			{
				JsonNode user = new JsonNode();
				user.setJsonNode("code", new JsonNode(((CybozuUser)value).getCode()));
				valueNode.setValue(user);
			}
			else if(value != null)
			{
				valueNode.setValue(value.toString());
			}
			codeNode.setJsonNode("value", valueNode);
				
			recNode.setJsonNode(code, codeNode);
		}
		for(Entry<String, KintoneSubTable> subTable: record.getSubTables().entrySet())
		{
			String code = subTable.getKey();
			JsonNode valueNode = new JsonNode();
			JsonNode codeNode  = new JsonNode();
			
			List<JsonNode> list = new ArrayList<JsonNode>();
			for(KintoneRecord subRecord: subTable.getValue())
			{
				JsonNode subRecNode = new JsonNode();
				JsonNode subValueNode = formatJsonNode(subRecord);
				if(subRecord.getId() != null && subRecord.getId() != 0)
				{
					JsonNode subIdNode = new JsonNode();
					subIdNode.setValue(subRecord.getId());
					subRecNode.setJsonNode("id", subIdNode);
				}
				subRecNode.setJsonNode("value", subValueNode);
				
				list.add(subRecNode);
			}
			valueNode.setArray(list);
			
			codeNode.setJsonNode("value", valueNode);
			recNode.setJsonNode(code, codeNode);
		}
		
		return recNode;
	}
	/**
	 * 複数選択項目の解析
	 * @param rootNode
	 * @return
	 */
	private Set<String> parseStringSet(JsonNode rootNode)
	{
		Set<String> values = new HashSet<>();
		for(JsonNode node: rootNode.getArray())
		{
			values.add(node.getTextValue());
		}
		return values;
	}
	/**
	 * User項目の解析
	 * @param rootNode
	 * @return
	 */
	private KintoneUserMap parseUserList(JsonNode rootNode)
	{
		KintoneUserMap users = new KintoneUserMap();
		
		for(JsonNode node: rootNode.getArray())
		{
			CybozuUser user = new CybozuUser();
			
			user.setCode(node.get("code").getTextValue());
			user.setName(node.get("name").getTextValue());
			users.put(node.get("code").getTextValue(), user);
		}
		return users;
	}
	/**
	 * Group項目の解析
	 * @param rootNode
	 * @return
	 */
	private KintoneGroupMap parseGroupList(JsonNode rootNode)
	{
		KintoneGroupMap groups = new KintoneGroupMap();
		
		for(JsonNode node: rootNode.getArray())
		{
			CybozuGroup group = new CybozuGroup();
			
			group.setCode(node.get("code").getTextValue());
			group.setName(node.get("name").getTextValue());
			groups.put(node.get("code").getTextValue(), group);
		}
		return groups;
	}
	/**
	 * 組織項目の解析
	 * @param rootNode
	 * @return
	 */
	private KintoneOrganizationMap parseOrganizationList(JsonNode rootNode)
	{
		KintoneOrganizationMap groups = new KintoneOrganizationMap();
		
		for(JsonNode node: rootNode.getArray())
		{
			CybozuOrganization group = new CybozuOrganization();
			
			group.setCode(node.get("code").getTextValue());
			group.setName(node.get("name").getTextValue());
			groups.put(node.get("code").getTextValue(), group);
		}
		return groups;
	}
	/**
	 * File項目の解析
	 * @param rootNode
	 * @return
	 */
	private KintoneFileList parseFileList(JsonNode rootNode)
	{
		KintoneFileList files = new KintoneFileList();
		
		for(JsonNode node : rootNode.getArray())
		{
			CybozuFile file = new CybozuFile();
			file.setContentType(node.get("contentType").getTextValue());
			file.setFileKey(node.get("fileKey").getTextValue());
			file.setName(node.get("name").getTextValue());
			file.setSize(node.get("size").getLongValue());
			files.add(file);
		}
		return files;
	}
	/**
	 * @param url
	 * @param method
	 * @param requestNode
	 * @param apiToken
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	private JsonNode restRequest(URL url, String method, JsonNode requestNode, String apiToken)
			throws KintoneRestException, IOException
	{
		return restRequest(url, method, requestNode, apiToken, ErrorType.NORMAL);
	}
	
	/**
	 * @param url
	 * @param method
	 * @param requestNode
	 * @param apiToken
	 * @param errorType
	 * @return
	 * @throws KintoneRestException
	 * @throws IOException
	 */
	private JsonNode restRequest(URL url, String method, JsonNode requestNode, String apiToken, ErrorType errorType)
		throws KintoneRestException, IOException
	{
		HttpURLConnection urlconn = null;
		DataOutputStream os = null;
		BufferedReader reader = null;
		
		try
		{
			String jsonString = null;
			
			urlconn = (HttpURLConnection)url.openConnection();
			setRequestHeader(urlconn, apiToken);
			//setBasicAuth(client);
			
			urlconn.setDoInput(true);
			urlconn.setUseCaches(false);
			urlconn.setRequestMethod(method);
			
			if(method.equals("GET"))
			{
				if(isDebugMode())
				{
					System.out.println(url.toString());
				}				
				urlconn.connect();
			}
			else
			{
				jsonString = requestNode.toString();
				if(isDebugMode())
				{
					System.out.println(jsonString);
				}
				jsonString = string2Unicode(jsonString);
				
				urlconn.setDoOutput(true);
				urlconn.setChunkedStreamingMode(0);
				urlconn.setRequestProperty("Content-Type", "application/json");
				urlconn.setRequestProperty("Content-Length", String.valueOf(jsonString.getBytes(Charset.forName("UTF-8")).length));
				os = new DataOutputStream(urlconn.getOutputStream());
				os.writeBytes(jsonString);
			}
			
			boolean isSuccess = urlconn.getResponseCode() == HttpURLConnection.HTTP_OK;
			
			reader = new BufferedReader(new InputStreamReader(isSuccess ? urlconn.getInputStream() : urlconn.getErrorStream(), Charset.forName("UTF-8")));
			
			StringBuilder sb = new StringBuilder();
			while(true)
			{
				String line = reader.readLine();
				if( line == null )
				{
					break;
				}
				sb.append(line);
			}
						
			JsonNode node = JsonNode.parse(unicode2String(sb.toString()));
			
			if(isDebugMode())
			{
				System.out.println(node.toString());
			}
			if(isSuccess)
			{
				return node;
			}
			else
			{
				throw new KintoneRestException(sb.toString());
				/*
				switch (errorType)
				{
				case NORMAL:
					
				default:
					for(JsonNode resp: node.get("results").getArray())
					{
						if(resp.isNull())
						{
							continue;
						}
						else
						{
							throw createException(resp);
						}
						
					}
					throw new KintoneRestException("");
				}*/
			}
		}
		finally
		{
			if(reader != null)
			{
				reader.close();
			}
			if(os != null)
			{
				os.close();
			}
			if(urlconn != null)
			{
				urlconn.disconnect();
			}
		}
	}
	/**
	 * Entity Nodeの変更
	 * @param entity
	 * @return
	 */
	private JsonNode getEntityNode(Object entity)
	{
		JsonNode entityNode = new JsonNode();
		if(entity instanceof CybozuUser)
		{
			entityNode.setJsonNode("type", new JsonNode("USER"));
			entityNode.setJsonNode("code", new JsonNode(((CybozuUser)entity).getCode()));
		}
		else if(entity instanceof CybozuOrganization)
		{
			entityNode.setJsonNode("type", new JsonNode("ORGANIZATION"));
			entityNode.setJsonNode("code", new JsonNode(((CybozuOrganization)entity).getCode()));
		}
		else if(entity instanceof CybozuGroup)
		{
			entityNode.setJsonNode("type", new JsonNode("GROUP"));
			entityNode.setJsonNode("code", new JsonNode(((CybozuGroup)entity).getCode()));
		}
		else
		{
			entityNode.setJsonNode("type", new JsonNode("FIELD_ENTITY"));
			entityNode.setJsonNode("code", new JsonNode(entity.toString()));
		}
		return entityNode;
	}
	/**
	 * 例外の生成
	 * @param node
	 * @return
	 */
	public KintoneRestException createException(JsonNode node)
	{
		KintoneRestException error = new KintoneRestException(node.toString());
		error.setCode(node.get("code").getTextValue());
		error.setId(node.get("id").getTextValue());
		error.setReason(node.get("message").getTextValue());
		return error;
	}
	/**
	 * String to Unicode
	 * @param value
	 * @return
	 */
	public static String string2Unicode(String value)
	{
		StringBuilder sb = new StringBuilder();
		char[] chars = value.toCharArray();
		
		for(int i=0; i<chars.length; i++)
		{
			char c1 = chars[i];
			if (c1 != 0x3C && c1 != 0x3E && 0x20 <= c1 && c1 <= 0x7E)
	        {
	            sb.append(c1);
	        }
	        else
	        {
	        	sb.append(String.format("\\u%04X", Character.codePointAt(chars, i)));
	        }
		}
		return sb.toString();
	}
	/**
	 * Unicode to String
	 * @param str
	 * @return
	 * @throws KintoneRestException
	 */
	private String unicode2String(String str) throws KintoneRestException
	{
		char[] chars = str.toCharArray();
		
		StringBuilder sb = new StringBuilder();
		for(int i=0; i<chars.length; i++)
		{
			char c1 = chars[i];
			if (c1 == '\\')
			{
				i++;
				if(chars.length == i)
				{
					throw new KintoneRestException(str);
				}
				char c2 = chars[i];	
				switch (c2)
				{
					case 'b':
						sb.append('\b');
						break;
					case 'f':
						sb.append('\f');
						break;
					case 'n':
						sb.append('\n');
						break;
					case 'r':
						sb.append('\r');
						break;
					case 't':
						sb.append('\t');
						break;
					case 'u':
						if(chars.length - i < 5)
						{
							throw new KintoneRestException(str);
						}
						sb.append(new String(new int[]{Integer.parseInt(new String(chars, i+1, 4), 16)}, 0, 1));
						i+=4;
						break;
					case '"':
					case '\\':
						sb.append(c2);
						break;
					default:
						throw new KintoneRestException(str);
				}
			}
			else
			{
				sb.append(c1);
			}
		}
		return sb.toString();
	}
	/**
	 * URLの取得
	 * @param spaceId space id
	 * @return URL
	 */
	private String getAppUrl(long spaceId)
	{
		return getUrl() + "/k" + (spaceId == NOT_SPACE_APP ? "/v1/" : "/guest/" + spaceId + "/v1/");
	}
}

